/* 
 * Cashbox is the legal property of its developers, whose names are listed in the copyright file included
 * with this source distribution.
 * 
 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
 * General Public License as published by the Free Software Foundation; either version 2 of the License,
 * or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
 * Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with this program; if not,
 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

#import "CBImporter.h"
#import "CBImportAccount.h"
#import "CBImportLabel.h"

#import <Cashbox/ColorListColors.h>

#import <Cashbox/CBAccount.h>
#import <Cashbox/CBTransaction.h>
#import <Cashbox/CBAttribute.h>
#import <Cashbox/CBLabel.h>

#import "CBInterfacePreferenceViewController.h"

#import <Cashbox/CBDataControllerProtocol.h>

#define ACCOUNT_NAME							@"Account Name"
#define ACCOUNT_ADDITIONAL						@"Account Additional Info"
#define ACCOUNT_KEYS							@"Account Keys"
#define ACCOUNT_TRANSACTIONS					@"Account Transactions"

#define TRANSACTION_ATTRIBUTES					@"Transaction Attributes"
#define TRANSACTION_DATE						@"Transaction Date"
#define TRANSACTION_RECONCILED					@"Transaction Reconciled"
#define TRANSACTION_CLEARED						@"Transaction Cleared"
#define TRANSACTION_LABEL_NAME					@"Transaction Label Name"
#define TRANSACTION_CREATED_TRANSFER			@"Transaction Created Transfer"

#define TRANSFER_FIRST_REFERENCE				@"First Reference"
#define TRANSFER_SECOND_REFERENCE				@"Second Reference"
#define TRANSFER_CREATED_OBJECT					@"Created Object"

NSString *CBImporterWillBeginReadingNotification = @"CBImporterWillBeginReadingNotification";
NSString *CBImporterDidFinishReadingNotification = @"CBImporterDidFinishReadingNotification";
NSString *CBImporterWillBeginImportingNotification = @"CBImporterWillBeginImportingNotification";
NSString *CBImporterDidFinishImportingNotification = @"CBImporterDidFinishImportingNotification";
NSString *CBImporterDidProgressImportingNotification = @"CBImporterDidProgressImportingNotification";

@interface CBImporter (Private)

- (NSMutableDictionary *)transferInfoForTransaction:(id <NSObject>)reference;
- (CBImportLabel *)notedLabelWithName:(NSString *)name;
- (void)processRead;
- (void)noteProgress:(NSString *)info percent:(float)percent;

@end

@implementation CBImporter

#pragma mark registering importers and getting info about registered importers
// ----------------------------------------------------------------------------------------------------
// registering importers and getting info about registered importers
// ----------------------------------------------------------------------------------------------------

static NSMutableArray *registeredImporters = nil;
+ (NSArray *)registeredImporters {
	return registeredImporters;
}

+ (NSArray *)handledExtensions {
	NSMutableArray *extensions = [NSMutableArray array];
	
	Class importerClass;
	NSEnumerator *enumerator = [registeredImporters objectEnumerator];
	while (importerClass = [enumerator nextObject]) {
		[extensions addObjectsFromArray:[importerClass canHandleExtensions]];
	}
	
	return extensions;	
}

+ (CBImporter *)importerForExtension:(NSString *)extension {
	Class importerClass;
	NSEnumerator *enumerator = [registeredImporters objectEnumerator];
	while (importerClass = [enumerator nextObject]) {
		if ([[importerClass canHandleExtensions] containsObject:extension]) {
			return [[[importerClass alloc] init] autorelease];
		}
	}
	return nil;
}

+ (void)registerImporter:(Class)importer {
	if ([importer isSubclassOfClass:[CBImporter class]]) {
		if (!registeredImporters) { registeredImporters = [[NSMutableArray alloc] init]; }
		[registeredImporters addObject:importer];		
	}
}
+ (NSArray *)canHandleExtensions {
	return nil;
}

#pragma mark create/destroy
// ----------------------------------------------------------------------------------------------------
// create/destroy
// ----------------------------------------------------------------------------------------------------

- (id)init {
	if (self = [super init]) {
		accounts = [[NSMutableArray alloc] init];
		orphanTransactions = [[NSMutableArray alloc] init];
		allTransactions = [[NSMutableArray alloc] init];
		transfers = [[NSMutableArray alloc] init];
		labels = [[NSMutableArray alloc] init];
		errors = [[NSMutableArray alloc] init];
	}
	return self;
}

- (void)dealloc {
	[accounts release];
	[orphanTransactions release];
	[allTransactions release];
	[transfers release];
	[labels release];
	[errors release];
	
	[super dealloc];
}


#pragma mark defaults and info
// ----------------------------------------------------------------------------------------------------
// defaults and info
// ----------------------------------------------------------------------------------------------------

+ (NSArray *)defaultKeys {
	return [NSArray arrayWithObjects:
		[NSNumber numberWithShort:CBDescriptionRole],
		[NSNumber numberWithShort:CBCheckNumberRole],
		[NSNumber numberWithShort:CBPositiveCashflowRole],
		[NSNumber numberWithShort:CBNegativeCashflowRole], nil];
}


#pragma mark getter/setter methods
// ----------------------------------------------------------------------------------------------------
// getter/setter methods
// ----------------------------------------------------------------------------------------------------

- (NSArray *)accounts {
	return accounts;
}

- (NSArray *)labels {
	return labels;
}

- (BOOL)importComplete {
	return importComplete;
}
- (BOOL)readComplete {
	return readComplete;
}

- (NSArray *)errors {
	return errors;
}
- (void)addError:(NSError *)err {
	[errors addObject:err];
}
- (void)clearErrors {
	[errors removeAllObjects];
}

#pragma mark reading and importing
// ----------------------------------------------------------------------------------------------------
// reading and importing
// ----------------------------------------------------------------------------------------------------

- (void)readFile:(NSString *)file {
	[[NSNotificationCenter defaultCenter] postNotificationName:CBImporterWillBeginReadingNotification
														object:self
													  userInfo:nil];
	[self addDataFromFile:file];
	[self processRead];
	readComplete = TRUE;
	[[NSNotificationCenter defaultCenter] postNotificationName:CBImporterDidFinishReadingNotification
														object:self
													  userInfo:nil];
}

- (void)processRead {
	if ([orphanTransactions count]) {
		CBImportAccount *ref = nil;
		
		NSString *originalName = @"Untitled";
		NSString *name = originalName;
		int number = 1;
		while ([CBAccount accountWithName:name]) {
			name = [originalName stringByAppendingFormat:@" %i", number++];
		}
		
		[self addAccountWithName:name
				  additionalInfo:@""
							keys:[[self class] defaultKeys]
					   reference:&ref];

		// we know that the reference is a CBImportAccount
		[(NSMutableDictionary *)[ref reference] setObject:orphanTransactions forKey:ACCOUNT_TRANSACTIONS];
	}
	
	if (![[self accounts] count]) {
		NSString *errorStr = FSLocalizedString(@"No accounts were found to import.", nil);
		NSDictionary *userInfoDict = [NSDictionary dictionaryWithObject:errorStr forKey:NSLocalizedDescriptionKey];
		[self addError:[NSError errorWithDomain:@"CBImporterDomain" code:CBCriticalImporterError userInfo:userInfoDict]];
	}
}

- (void)importDataWillBegin { }
- (void)importDataDidFinish { }

- (void)importData {
	
	if (![[self errors] count]) {
		[[NSNotificationCenter defaultCenter] postNotificationName:CBImporterWillBeginImportingNotification
															object:self
														  userInfo:nil];		
		[self importDataWillBegin];
		
		CBImportLabel *label;
		NSEnumerator *labelEnumerator = [[self labels] objectEnumerator];
		while (label = [labelEnumerator nextObject]) {
			CBLabel *newLabel = [[[CBLabel alloc] initWithIndex:0 andInsertIntoManagedObjectContext:[[cashbox dataController] managedObjectContext]] autorelease];
			[newLabel setName:[label name]];
			[newLabel setColor:[label color]];
		}

		BOOL warnedAboutLabels = FALSE;
		int accountsToImport = 0, accountNumber = 0;
		CBImportAccount *account;
		NSEnumerator *accountEnumerator;

		// count the accounts to import
		accountEnumerator = [[self accounts] objectEnumerator];
		while (account = [accountEnumerator nextObject]) {
			if ([account import]) { accountsToImport++; }
		}
		
		accountEnumerator = [[self accounts] objectEnumerator];
		while (account = [accountEnumerator nextObject]) {
			if ([account import]) {
				
				NSString *progressDescriptionString = [NSString stringWithFormat:FSLocalizedString(@"Importing %@...", nil), [account name]];
				[self noteProgress:progressDescriptionString percent:(float)accountNumber / (float)accountsToImport];
				
				NSDictionary *accountData = (NSDictionary *)[account reference];
				CBAccount *importTo = nil;
				
				if ([account createAccount]) {
					importTo = [[[[account createAccount] alloc] init] autorelease];
					[importTo setName:[accountData objectForKey:ACCOUNT_NAME]];
					[importTo setAdditionalInfo:[accountData objectForKey:ACCOUNT_ADDITIONAL]];
				} else if ([account mergeAccount]) {
					importTo = [account mergeAccount];
				}
				
				if (importTo) {
					int transactionNumber = 0;
					int transactionsToImport = [[accountData objectForKey:ACCOUNT_TRANSACTIONS] count];
					
					NSMutableDictionary *transaction;
					NSMutableSet *transactionsSet = [NSMutableSet setWithCapacity:transactionsToImport];
					NSEnumerator *transactionEnumerator = [[accountData objectForKey:ACCOUNT_TRANSACTIONS] objectEnumerator];
					while (transaction = [transactionEnumerator nextObject]) {

						if (transactionNumber % 7 == 0) { // show progress every 7 transactions
							[self noteProgress:progressDescriptionString percent:(float)accountNumber / (float)accountsToImport + (float)transactionNumber / (float)transactionsToImport / (float)accountsToImport];
						}

						NSManagedObjectContext *context = [[cashbox dataController] managedObjectContext];
						Class transactionClass = [[importTo class] transactionClass];
						CBTransaction *newTransaction =
							[[[transactionClass alloc] initWithEntity:[transactionClass entity]
									   insertIntoManagedObjectContext:context] autorelease];
						
						// try to link transfers
						NSMutableDictionary *transferInfo;
						if (transferInfo = [self transferInfoForTransaction:transaction]) {
							// if the other transfer transaction was already created
							CBTransaction *transfer;
							if (transfer = [transferInfo objectForKey:TRANSFER_CREATED_OBJECT]) {
								[newTransaction setDate:nil]; // clear date for now to allow setting transfer
								[transfer setTransfer:newTransaction];
								[transfers removeObject:transferInfo];
							} else {
								[transferInfo setObject:newTransaction forKey:TRANSFER_CREATED_OBJECT];							
							}
						}
						
						NSDictionary *transactionAttributes = [transaction objectForKey:TRANSACTION_ATTRIBUTES];
						NSNumber *attributeKey;
						NSEnumerator *attributesEnumerator = [[accountData objectForKey:ACCOUNT_KEYS] objectEnumerator];
						while (attributeKey = [attributesEnumerator nextObject]) {
							id value = [transactionAttributes objectForKey:attributeKey];
							if (value != [NSNull null]) {
								[newTransaction setValue:value forRole:[attributeKey shortValue]];							
							}
						}

						// set the date
						[newTransaction setDate:[transaction objectForKey:TRANSACTION_DATE]];

						// set the state
						if ([[transaction objectForKey:TRANSACTION_CLEARED] boolValue] &&
							[[NSUserDefaults standardUserDefaults] boolForKey:CBPostedStatePreferenceKey]) { [newTransaction setState:CBPostedState]; }
						if ([[transaction objectForKey:TRANSACTION_RECONCILED] boolValue]) { [newTransaction setState:CBReconciledState]; }

						// set the label name if one exists
						NSString *labelName = [transaction objectForKey:TRANSACTION_LABEL_NAME];
						if (labelName && ((id)labelName != [NSNull null])) {
							CBLabel *label = [CBLabel labelWithName:labelName];
							if (label) {
								[newTransaction setLabel:label];								
							} else {
								if (!warnedAboutLabels) {
									FSLog(@"could not find label named %@", labelName);
									// warn about label not being added
									NSString *errorStr = FSLocalizedString(@"Some imported transactions may be missing labels.", nil);
									NSDictionary *userInfoDict = [NSDictionary dictionaryWithObject:errorStr forKey:NSLocalizedDescriptionKey];
									[self addError:[NSError errorWithDomain:@"CBImporterDomain" code:CBCautionaryImporterError userInfo:userInfoDict]];
									warnedAboutLabels = TRUE;
								}
							}
						}
											
						[transactionsSet addObject:newTransaction];
						transactionNumber++;
					}
					[importTo addTransactions:transactionsSet];
				}
				accountNumber++;
			}
		}
		
		
		if ([transfers count]) {
			// warn that not all transfers were linked
			NSString *errorStr = FSLocalizedString(@"Not all transfers were linked.  This could have been caused by not importing all accounts.", nil);
			NSDictionary *userInfoDict = [NSDictionary dictionaryWithObject:errorStr forKey:NSLocalizedDescriptionKey];
			[self addError:[NSError errorWithDomain:@"CBImporterDomain" code:CBCautionaryImporterError userInfo:userInfoDict]];
		}

		importComplete = TRUE;
		[self importDataDidFinish];
		[[NSNotificationCenter defaultCenter] postNotificationName:CBImporterDidFinishImportingNotification
															object:self
														  userInfo:nil];		
	}
}

- (void)noteProgress:(NSString *)info percent:(float)percent {
	NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
		info, @"CBDescription",
		[NSNumber numberWithFloat:percent], @"CBPercent", nil];
	
	[[NSNotificationCenter defaultCenter] postNotificationName:CBImporterDidProgressImportingNotification
														object:self
													  userInfo:userInfo];
}

#pragma mark adding/altering accounts
// ----------------------------------------------------------------------------------------------------
// adding/altering accounts
// ----------------------------------------------------------------------------------------------------

- (void)addAccountWithName:(NSString *)name
			additionalInfo:(NSString *)additionalInfo
					  keys:(NSArray *)keys
				 reference:(id <NSObject> *)ref {
	
	NSAssert(name != nil, @"-[CBImporter addAccountWithName:additionalInfo:keys:reference:] name must not be nil");
	NSAssert(additionalInfo != nil, @"-[CBImporter addAccountWithName:additionalInfo:keys:reference:] additionalInfo must not be nil");
	NSAssert(keys != nil, @"-[CBImporter addAccountWithName:additionalInfo:keys:reference:] keys must not be nil");
	
	NSMutableDictionary *account = [NSMutableDictionary dictionaryWithObjectsAndKeys:
		name, ACCOUNT_NAME,
		additionalInfo, ACCOUNT_ADDITIONAL,
		keys, ACCOUNT_KEYS,
		[NSMutableArray array], ACCOUNT_TRANSACTIONS, nil];
	
	CBImportAccount *importAccount = [CBImportAccount importAccountWithName:name keys:keys reference:account];
	[self willChangeValueForKey:@"accounts"];
	[accounts addObject:importAccount];
	[self didChangeValueForKey:@"accounts"];
	
	if (ref != NULL) { *ref = importAccount; }
}


- (void)updateAccountKeys:(NSArray *)keys
				reference:(id <NSObject>)accountReference {
	
	// verify that the passed in reference is valid
	// and that it's an account object that we created here
	if (accountReference && ([accounts indexOfObjectIdenticalTo:accountReference] != NSNotFound)) {
		[(CBImportAccount *)accountReference setKeys:keys];
		[(NSMutableDictionary *)[(CBImportAccount *)accountReference reference] setObject:keys forKey:ACCOUNT_KEYS];
	}
}

#pragma mark adding/altering transactions
// ----------------------------------------------------------------------------------------------------
// adding/altering transactions
// ----------------------------------------------------------------------------------------------------

- (void)addTransactionToAccount:(id <NSObject>)accountReference
				 withAttributes:(NSDictionary *)attributes
						   date:(NSDate *)date
					 reconciled:(BOOL)reconciled
						cleared:(BOOL)cleared
					  labelName:(NSString *)label
					  reference:(id <NSObject> *)ref {
	
	NSMutableArray *transactions = nil;
		
	// verify that the passed in reference is valid
	// and that it's an account object that we created here
	if (accountReference && ([accounts indexOfObjectIdenticalTo:accountReference] != NSNotFound)) {
		transactions = [(NSDictionary *)[(CBImportAccount *)accountReference reference] objectForKey:ACCOUNT_TRANSACTIONS];
	} else if (!accountReference) {
		// also allowed to add transactions not associated with an account
		transactions = orphanTransactions;
	}
	
	if (transactions) {
		NSMutableDictionary *transaction = [NSMutableDictionary dictionary];
		if (attributes) { [transaction setObject:attributes forKey:TRANSACTION_ATTRIBUTES]; }
		if (date) { [transaction setObject:date forKey:TRANSACTION_DATE]; }
		[transaction setObject:[NSNumber numberWithBool:reconciled] forKey:TRANSACTION_RECONCILED];
		[transaction setObject:[NSNumber numberWithBool:cleared] forKey:TRANSACTION_CLEARED];
		if (label) { [transaction setObject:label forKey:TRANSACTION_LABEL_NAME]; }
		
		[transactions addObject:transaction];
		[allTransactions addObject:transaction];
		
		if (ref != NULL) { *ref = transaction; }		
	}	
}

- (void)updateTransactionDate:(NSDate *)date
					reference:(id <NSObject>)transactionReference {
	
	// verify that the passed in reference is valid
	// and that it's a transaction object that we created here
	if (transactionReference && ([allTransactions indexOfObjectIdenticalTo:transactionReference] != NSNotFound)) {
		if (date) {
			[(NSMutableDictionary *)transactionReference setObject:date forKey:TRANSACTION_DATE];
		} else {
			[(NSMutableDictionary *)transactionReference removeObjectForKey:TRANSACTION_DATE];
		}
	}	
}

#pragma mark transfers
// ----------------------------------------------------------------------------------------------------
// transfers
// ----------------------------------------------------------------------------------------------------

- (NSMutableDictionary *)transferInfoForTransaction:(id <NSObject>)reference {
	NSMutableDictionary *info;
	NSEnumerator *enumerator = [transfers objectEnumerator];
	while (info = [enumerator nextObject]) {
		if (reference == [info objectForKey:TRANSFER_FIRST_REFERENCE] ||
			reference == [info objectForKey:TRANSFER_SECOND_REFERENCE]) {
			return info;
		}
	}
	return nil;
}

- (void)createTransfersFor:(id <NSObject>)firstReference
					andFor:(id <NSObject>)secondReference {
	
	// verify that the passed in reference is valid
	// and that it's a transaction object that we created here
	if (firstReference && secondReference &&
		[allTransactions indexOfObjectIdenticalTo:firstReference] != NSNotFound &&
		[allTransactions indexOfObjectIdenticalTo:secondReference] != NSNotFound) {
		
		[transfers addObject:[NSMutableDictionary dictionaryWithObjectsAndKeys:
			firstReference, TRANSFER_FIRST_REFERENCE,
			secondReference, TRANSFER_SECOND_REFERENCE,
			nil]];
	}
	
}

#pragma mark labels
// ----------------------------------------------------------------------------------------------------
// labels
// ----------------------------------------------------------------------------------------------------

- (CBImportLabel *)notedLabelWithName:(NSString *)name {
	CBImportLabel *label;
	NSEnumerator *labelEnumerator = [labels objectEnumerator];
	while (label = [labelEnumerator nextObject]) {
		if ([[label name] isEqualToString:name]) {
			return label;
		}
	}
	return nil;
}

- (void)noteLabel:(NSString *)name
		withColor:(NSColor *)color {
	
	static int lastIndex = 0;
	static NSArray *array = nil;

	if (![CBLabel labelWithName:name] && ![self notedLabelWithName:name]) {
		if (!array) { array = [COLOR_LIST_COLOR_ARRAY retain]; }
		if (!color) { color = [array objectAtIndex:lastIndex++ % [array count]]; }
		[self willChangeValueForKey:@"labels"];
		[labels addObject:[CBImportLabel importLabelWithName:name color:color]];		
		[self didChangeValueForKey:@"labels"];
	}
}

@end
